﻿using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.EventArgs;
using RaceHFTools.Utils;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace RaceHFTools.BluetoothLE.Bean.Module
{
    public enum FSYNC_TYPE
    {
        ADDRESS,        // for flash device
        FILE,           // for filesystem device

        ERROR = 0xff
    };

    public enum FSYNC_OPERATION
    {
        INQUIRY,
        SYNC,
        START,
        SEND,
        STOP,
        TERMINAL,
        HEARTBEAT,

        ERROR = 0xff
    };

    public enum FSYNC_ACK
    {
        OK,
        DONE,
        EXIT,
        ERR_OP,
        ERR_PROCESS,
        ERR_HARD,
        ERR_RAM,
        ERR_OVERFLOW,
        ERR_OPENFILE,
        ERR_CRC,

        UNKOWN = 0xff
    };

    public enum FSYNC_INQUIRY
    {
        VERSION,
        LINEMAX,
        OPERATION,
        STATE,
        UPLOAD,

        ERROR = 0xff
    };

    public enum FSYNC_HARD
    {
        RAM,
        FLASH,
        SDCARD,

        ERROR = 0xff
    };

    public enum FSYNC_PATH_TYPE
    {
        DIRECTORY,
        FILE,

        ERROR = 0xff,
    }

    public struct FileItem
    {
        public int Index;
        public FSYNC_PATH_TYPE Type;
        public string Name;
        public int Size;
    };

    public class Fsync
    {
        public delegate void ProgressEvent(string process, int percent = 0);

        public readonly Guid FSYNC_CP_UUID = new Guid("0000eee1-0000-1000-8000-00805f9b34fb");
        public readonly Guid FSYNC_DATA_UUID = new Guid("0000eee2-0000-1000-8000-00805f9b34fb");

        const int FSYNC_TIMEOUT = 1000;

        readonly Bean Bean;
        readonly ICharacteristic cp_char, data_char;
        readonly Semaphore cp_ntf_sem, data_ntf_sem;
        readonly Mutex cp_ntf_mux, data_ntf_mux;
        readonly Queue<byte[]> cp_ntf_que, data_ntf_que;

        public Fsync(Bean bean)
        {
            Bean = bean;
            if (Bean == null)
                throw new Exception("No valid device");

            cp_char = Bean.Transport.FindCharacteristicById(FSYNC_CP_UUID);
            data_char = Bean.Transport.FindCharacteristicById(FSYNC_DATA_UUID);
            if (cp_char == null || data_char == null)
                throw new Exception("No valid characteristic");
            cp_char.WriteType = CharacteristicWriteType.WithoutResponse;

            cp_ntf_sem = new Semaphore(0, 1);
            cp_ntf_mux = new Mutex();
            cp_ntf_que = new Queue<byte[]>();
            data_ntf_sem = new Semaphore(0, 1);
            data_ntf_mux = new Mutex();
            data_ntf_que = new Queue<byte[]>();
        }

        void NotifyHanlder(object sender, CharacteristicUpdatedEventArgs e)
        {
            ICharacteristic characteristic = e.Characteristic;
            byte[] val = characteristic.Value;

            if (characteristic == cp_char)
            {
                cp_ntf_mux.WaitOne();
                cp_ntf_que.Enqueue(val);
                cp_ntf_sem.WaitOne(0);
                cp_ntf_sem.Release();
                cp_ntf_mux.ReleaseMutex();
            }
            else if (characteristic == data_char)
            {
                data_ntf_mux.WaitOne();
                data_ntf_que.Enqueue(val);
                data_ntf_sem.WaitOne(0);
                data_ntf_sem.Release();
                data_ntf_mux.ReleaseMutex();
            }
        }

        byte[] WaitNotify(ICharacteristic characteristic, int timeout)
        {
            byte[] data = null;

            if (characteristic == cp_char)
            {
                if (cp_ntf_que.Count == 0)
                {
                    if (!cp_ntf_sem.WaitOne(timeout))
                        return null;
                }
                cp_ntf_mux.WaitOne();
                data = cp_ntf_que.Dequeue();
                cp_ntf_sem.WaitOne(0);
                cp_ntf_mux.ReleaseMutex();
            }
            else if (characteristic == data_char)
            {
                if (data_ntf_que.Count == 0)
                {
                    if (!data_ntf_sem.WaitOne(timeout))
                        return null;
                }
                data_ntf_mux.WaitOne();
                data = data_ntf_que.Dequeue();
                data_ntf_sem.WaitOne(0);
                data_ntf_mux.ReleaseMutex();
            }

            return data;
        }

        void ClearNotifyQueue()
        {
            cp_ntf_que.Clear();
            cp_ntf_sem.WaitOne(0);
            data_ntf_que.Clear();
            data_ntf_sem.WaitOne(0);
        }


        async Task<uint> InquiryVersion()
        {
            uint version = 0;

            await Bean.Transport.Write(cp_char, new byte[] { (byte)FSYNC_OPERATION.INQUIRY, (byte)FSYNC_INQUIRY.VERSION });
            byte[] ack = WaitNotify(cp_char, FSYNC_TIMEOUT);
            if (ack != null && ack.Length == 2 && ack[0] == (byte)FSYNC_OPERATION.INQUIRY)
                version = ack[1];

            return version;
        }

        async Task<uint> InquiryLineMax()
        {
            uint linemax = 0;

            await Bean.Transport.Write(cp_char, new byte[] { (byte)FSYNC_OPERATION.INQUIRY, (byte)FSYNC_INQUIRY.LINEMAX });
            byte[] ack = WaitNotify(cp_char, FSYNC_TIMEOUT);
            if (ack != null && ack.Length == 3 && ack[0] == (byte)FSYNC_OPERATION.INQUIRY)
                linemax = BitConverter.ToUInt16(ack, 1);

            return linemax;
        }

        async Task<uint> FsyncOperateStart(string file)
        {
            uint size = 0;

            byte[] hard_bitarray = { (byte)FSYNC_HARD.SDCARD };
            byte[] path_bitarray = Encoding.UTF8.GetBytes(file);
            byte[] start_bitarray = new byte[3 + path_bitarray.Length];

            start_bitarray[0] = (byte)FSYNC_OPERATION.START;
            hard_bitarray.CopyTo(start_bitarray, 1);
            path_bitarray.CopyTo(start_bitarray, 2);

            await Bean.Transport.Write(cp_char, start_bitarray);
            byte[] ack = WaitNotify(cp_char, FSYNC_TIMEOUT);
            if (ack != null && ack.Length == 5 && ack[0] == (byte)FSYNC_OPERATION.START)
                size = BitConverter.ToUInt32(ack, 1);

            return size;
        }

        async Task<bool> FsyncOperateSend()
        {
            bool send_ok = false;

            await Bean.Transport.Write(cp_char, new byte[] { (byte)FSYNC_OPERATION.SEND });
            byte[] ack = WaitNotify(cp_char, FSYNC_TIMEOUT);
            if (ack != null && ack.Length == 2 && ack[0] == (byte)FSYNC_OPERATION.SEND)
                send_ok = ack[1] == (byte)FSYNC_ACK.OK;

            return send_ok;
        }

        byte[] FsyncOperateSync(uint filesize, ProgressEvent progress, CancellationToken ct)
        {
            bool sync_ok = false;

            progress?.Invoke("同步中");

            Timer heart_timer = new Timer((s) =>
            {
                if (Bean.IsConnect)
                    _ = Bean.Transport.Write(cp_char, new byte[] { (byte)FSYNC_OPERATION.HEARTBEAT });
            }, null, 500, 1000);

            uint recvlen = 0;
            byte[] filebuff = new byte[filesize];
            long m_stick = DateTime.Now.Ticks;
            uint m_rlen = 0;
            double m_spd = 0;
            do
            {
                byte[] val = WaitNotify(data_char, FSYNC_TIMEOUT);
                if (val == null)
                    break;
                int leftlen = (int)filesize - (int)recvlen;
                uint oplen = (uint)(val.Length < leftlen ? val.Length : leftlen);
                val.CopyTo(filebuff, recvlen);
                recvlen += oplen;

                long dt = DateTime.Now.Ticks - m_stick;
                if (dt / 10 > 1000 * 1000)
                {
                    m_spd = (double)(recvlen - m_rlen) / (double)(dt * (1e-7)) / (double)1e3;
                    m_stick += dt;
                    m_rlen = recvlen;
                }

                progress?.Invoke($"同步中 {m_spd:F2}KB/S", (int)(100 * recvlen / filesize));
            } while (recvlen < filesize && !ct.IsCancellationRequested);

            heart_timer.Dispose();

            if (!ct.IsCancellationRequested)
            {
                progress?.Invoke("校验中");

                byte[] ack = WaitNotify(cp_char, FSYNC_TIMEOUT);
                if (ack != null && ack.Length == 6 && ack[0] == (byte)FSYNC_OPERATION.TERMINAL && ack[1] == (byte)FSYNC_ACK.DONE)
                {
                    uint fcrc = BitConverter.ToUInt32(ack, 2);
                    uint crc = CRC.GetCRC32(filebuff);
                    if (crc == fcrc)
                        sync_ok = true;
                }
            }

            return sync_ok ? filebuff : null;
        }

        async Task<bool> FsyncOperateStop()
        {
            bool stop_ok = false;

            await Bean.Transport.Write(cp_char, new byte[] { (byte)FSYNC_OPERATION.STOP });
            byte[] ack = WaitNotify(cp_char, FSYNC_TIMEOUT);
            if (ack != null && ack.Length == 2 && ack[0] == (byte)FSYNC_OPERATION.STOP)
                stop_ok = ack[1] == (byte)FSYNC_ACK.OK;

            return stop_ok;
        }

        async Task<FileItem[]> FsyncReadFileList(string path, uint linemax, ProgressEvent progress, CancellationToken ct)
        {
            List<FileItem> filelist = new List<FileItem>();

            bool on_data_handler(byte[] data)
            {
                if (data == null || data.Length < 5 || data[0] != (byte)FSYNC_OPERATION.SYNC)
                    return false;

                int offset = 1;
                FileItem item = new FileItem();
                item.Index = BitConverter.ToUInt16(data, offset); offset += 2;
                item.Type = (FSYNC_PATH_TYPE)data[offset]; offset += 1;
                if (item.Type == FSYNC_PATH_TYPE.FILE)
                {
                    item.Size = BitConverter.ToInt32(data, offset);
                    offset += 4;
                }

                int namelen = 0;
                for (int i = offset; i < data.Length && data[i] != 0; i++)
                    namelen++;
                if (namelen > 0)
                {
                    byte[] namebytes = new byte[namelen + 1];
                    Array.Copy(data, offset, namebytes, 0, namelen);
                    item.Name = Encoding.UTF8.GetString(namebytes);
                    filelist.Add(item);
                }

                return true;
            }

            uint predict_files = 0;
            progress?.Invoke("同步中");
            do
            {
                predict_files += linemax;

                byte[] sync_hard_bitarray = { (byte)FSYNC_HARD.SDCARD };
                byte[] sync_index_bitarray = BitConverter.GetBytes(filelist.Count);
                byte[] sync_path_bitarray = Encoding.UTF8.GetBytes(path);
                byte[] start_bitarray = new byte[4 + path.Length + 1];

                start_bitarray[0] = (byte)FSYNC_OPERATION.SYNC;
                sync_hard_bitarray.CopyTo(start_bitarray, 1);
                sync_index_bitarray.CopyTo(start_bitarray, 2);
                sync_path_bitarray.CopyTo(start_bitarray, 4);

                await Bean.Transport.Write(cp_char, start_bitarray);

                while (on_data_handler(WaitNotify(cp_char, FSYNC_TIMEOUT))) { }

                progress?.Invoke("同步中", filelist.Count);
            } while (filelist.Count == predict_files && !ct.IsCancellationRequested);

            return filelist.ToArray();
        }

        public async Task<FileItem[]> ReadFileList(string path, ProgressEvent progress = null, CancellationToken ct = default)
        {
            uint version = 0;
            uint linemax = 0;
            FileItem[] filelist = null;
            ClearNotifyQueue();

            progress?.Invoke("准备中");
            cp_char.ValueUpdated += NotifyHanlder;
            await Bean.Transport.EnableNotify(cp_char);
            await Task.Delay(20);

            progress?.Invoke("握手中");
            // FSYNC_INQUIRY_VERSION
            version = await InquiryVersion();
            if (version != 1 || ct.IsCancellationRequested)
                goto error;
            await Task.Delay(20);

            // FSYNC_INQUIRY_LINEMAX
            linemax = await InquiryLineMax();
            if (linemax == 0 || ct.IsCancellationRequested)
                goto error;
            await Task.Delay(20);

            // FSYNC_OP_SYNC
            filelist = await FsyncReadFileList(path, linemax, progress, ct);
            await Task.Delay(20);

        error:
            cp_char.ValueUpdated -= NotifyHanlder;
            await Bean.Transport.DisableNotify(cp_char);
            return filelist;
        }

        public async Task<byte[]> SyncFile(string file, ProgressEvent progress = null, CancellationToken ct = default)
        {
            uint version = 0;
            uint filesize = 0;
            byte[] filebuff = null;
            ClearNotifyQueue();

            progress?.Invoke("准备中");
            cp_char.ValueUpdated += NotifyHanlder;
            data_char.ValueUpdated += NotifyHanlder;
            await Bean.Transport.EnableNotify(cp_char);
            await Bean.Transport.EnableNotify(data_char);
            await Task.Delay(20);

            progress?.Invoke("握手中");
            // FSYNC_INQUIRY_VERSION
            version = await InquiryVersion();
            if (version != 1 || ct.IsCancellationRequested)
                goto error;
            await Task.Delay(20);

            // FSYNC_OP_START
            filesize = await FsyncOperateStart(file);
            if (filesize <= 0 || ct.IsCancellationRequested)
                goto error;
            filebuff = new byte[filesize];
            await Task.Delay(20);

            // FSYNC_OP_SEND
            if (await FsyncOperateSend() == false || ct.IsCancellationRequested)
                goto error;
            filebuff = FsyncOperateSync(filesize, progress, ct);
            await Task.Delay(20);

            // FSYNC_OP_STOP
            // operate_stop();

        error:
            if (ct.IsCancellationRequested)
                await FsyncOperateStop();
            cp_char.ValueUpdated -= NotifyHanlder;
            data_char.ValueUpdated -= NotifyHanlder;
            await Bean.Transport.DisableNotify(cp_char);
            await Bean.Transport.DisableNotify(data_char);
            return ct.IsCancellationRequested ? null : filebuff;
        }
    }
}
